Skip to content

Comments

feat: add debug_getRawReceipts method#4919

Open
BartoszSolkaBD wants to merge 18 commits intohiero-ledger:mainfrom
BartoszSolkaBD:4892-Add-debug_getRawReceipts-method
Open

feat: add debug_getRawReceipts method#4919
BartoszSolkaBD wants to merge 18 commits intohiero-ledger:mainfrom
BartoszSolkaBD:4892-Add-debug_getRawReceipts-method

Conversation

@BartoszSolkaBD
Copy link
Contributor

@BartoszSolkaBD BartoszSolkaBD commented Feb 13, 2026

Description

This PR adds support for the debug_getRawReceipts JSON-RPC method. It returns an array of EIP-2718 binary-encoded transaction receipts for a given block.

Changes:

  • Relay: New getRawReceipts(blockNumber, requestDetails) on the Debug interface.
  • Receipt encoding: New packages/relay/src/lib/receiptSerialization.ts that encodes a single receipt to EIP-2718 hex (Yellow Paper 4-field RLP: postState/root, cumulativeGasUsed, logsBloom, logs; optional type-byte prefix for typed transactions).
  • Docs: debug_getRawReceipts added to docs/openrpc.json and docs/rpc-api.md.
  • Tests:
    • Unit (packages/relay/tests/lib/debug.spec.ts): empty array when block not found, empty array when block has no receipts, multiple receipts, single receipt with logs; UNSUPPORTED_METHOD when debug API is disabled; invalid block number → INVALID_PARAMETER.
    • Acceptance (packages/server/tests/acceptance/debug.spec.ts): happy path for a block with transactions (array of hex strings), invalid block number.
    • Conformity: Overwrite packages/server/tests/acceptance/data/conformity/overwrites/debug_getRawReceipts/get-block-n.io.
    • debug_getRawReceipts.spec.ts - round-trip decode assertions (RLP decode and compare to stubbed receipt fields)

Related issue(s)

Fixes #4892

Testing Guide

  1. Run the relay against mainnet
    Start the JSON-RPC relay with mainnet config (and DEBUG_API_ENABLED=true if required). Ensure it is connected and serving requests.
  2. Call debug_getRawReceipts for block with non-empty receipts.
   {"jsonrpc":"2.0","id":1,"method":"debug_getRawReceipts","params":["0x56e86ab"]}
  1. Validate against eth_getBlockReceipts for the same block
   {"jsonrpc":"2.0","id":1,"method":"eth_getBlockReceipts","params":["0x56e86ab"]}

Changes from original design (optional)

N/A

Additional work needed (optional)

N/A

Checklist

  • I've assigned an assignee to this PR and related issue(s) (if applicable)
  • I've assigned a label to this PR and related issue(s) (if applicable)
  • I've assigned a milestone to this PR and related issue(s) (if applicable)
  • I've updated documentation (code comments, README, etc. if applicable)
  • I've done sufficient testing (unit, integration, etc.)

Signed-off-by: Bartosz Solka <bartosz.solka@blockydevs.com>
Signed-off-by: Bartosz Solka <bartosz.solka@blockydevs.com>
…ero-ledger#4889)

Signed-off-by: Bartosz Solka <bartosz.solka@blockydevs.com>
Signed-off-by: Bartosz Solka <bartosz.solka@blockydevs.com>
…r#4889)

Signed-off-by: Bartosz Solka <bartosz.solka@blockydevs.com>
…4889)

Signed-off-by: Bartosz Solka <bartosz.solka@blockydevs.com>
Signed-off-by: Bartosz Solka <bartosz.solka@blockydevs.com>
Signed-off-by: BartoszSolkaBD <bartosz.solka@blockydevs.com>
@jasuwienas jasuwienas added the enhancement New feature or request label Feb 13, 2026
@jasuwienas jasuwienas added this to the 0.75.0 milestone Feb 13, 2026
Copy link
Contributor

@jasuwienas jasuwienas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LG!

The algorithm appears to match my understanding from the Yellow Paper (and the comments align as well). However, it’s all a little bit complicated for me, that's why I proposed this: https://github.com/hiero-ledger/hiero-json-rpc-relay/pull/4919/changes#r2804276011 .

…eipts and eth_getBlockReceipts (hiero-ledger#4892)

Signed-off-by: Bartosz Solka <bartosz.solka@blockydevs.com>
…larity (hiero-ledger#4892)

Signed-off-by: Bartosz Solka <bartosz.solka@blockydevs.com>
Copy link
Contributor

@jasuwienas jasuwienas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LG! The data we used in one of the unit test as input is the same as:

curl -sS "https://docs-demo.monad-mainnet.quiknode.pro/"  -H 'Content-Type: application/json' --data '{"jsonrpc":"2.0","id":1,"method":"eth_getRawReceipts","params":["0x23592c0"]}'

and we get the expect result the same as:

curl -sS "https://docs-demo.monad-mainnet.quiknode.pro/"  -H 'Content-Type: application/json' --data '{"jsonrpc":"2.0","id":1,"method":"debug_getRawReceipts","params":["0x23592c0"]}'

It’s not directly related to this change, but debug.spec.ts has grown quite large. We could make it much smaller by moving the test cases into separate file(s).

Signed-off-by: Bartosz Solka <bartosz.solka@blockydevs.com>
…ementation (hiero-ledger#4892)

Signed-off-by: Bartosz Solka <bartosz.solka@blockydevs.com>
@simzzz simzzz modified the milestones: 0.75.0, 0.76.0 Feb 16, 2026
…penrpc-json-updated (hiero-ledger#4892)

Signed-off-by: Bartosz Solka <bartosz.solka@blockydevs.com>
jasuwienas
jasuwienas previously approved these changes Feb 17, 2026
…Receipts-method

Signed-off-by: Bartosz Solka <bartosz.solka@blockydevs.com>
simzzz
simzzz previously approved these changes Feb 17, 2026
});
});

describe('debug_getRawReceipts', () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it worth adding more advanced checks like these here https://github.com/hiero-ledger/hiero-json-rpc-relay/pull/4909/changes#diff-ce8ccd45e52cf90b1bd467b689269510421319c309b4e08a8232707ebe3aa63bR1005?

I think the most beneficial tests are e2e, and from the beginning, we're trying to make them as precise as we can.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we have a test here https://github.com/hiero-ledger/hiero-json-rpc-relay/pull/4919/changes#diff-a0d7caed18547ec84f2f466ff3e64154ff1eae3f7d6c6f8ae25c9a3153f5e52cR2030 that validates the fields individually. Please let me know if you think this is sufficient, or if we should also add similar checks to the acceptance tests.

@BartoszSolkaBD BartoszSolkaBD marked this pull request as draft February 18, 2026 10:33
…ceipts functionality (hiero-ledger#4892)

Signed-off-by: Bartosz Solka <bartosz.solka@blockydevs.com>
…ger#4892)

Signed-off-by: Bartosz Solka <bartosz.solka@blockydevs.com>
@BartoszSolkaBD BartoszSolkaBD marked this pull request as ready for review February 19, 2026 12:02
…Receipts-method

Signed-off-by: Bartosz Solka <bartosz.solka@blockydevs.com>
…ionReceiptFactory (hiero-ledger#4892)

Signed-off-by: Bartosz Solka <bartosz.solka@blockydevs.com>
@codecov
Copy link

codecov bot commented Feb 20, 2026

Codecov Report

❌ Patch coverage is 80.00000% with 66 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...ib/services/ethService/blockService/blockWorker.ts 81.06% 32 Missing ⚠️
packages/relay/src/lib/types/IReceiptRlpInput.ts 0.00% 24 Missing ⚠️
...s/relay/src/lib/services/workersService/workers.ts 0.00% 10 Missing ⚠️
@@            Coverage Diff             @@
##             main    #4919      +/-   ##
==========================================
- Coverage   95.86%   92.01%   -3.86%     
==========================================
  Files         143      115      -28     
  Lines       23968    19924    -4044     
  Branches     1900     1474     -426     
==========================================
- Hits        22977    18333    -4644     
- Misses        967     1525     +558     
- Partials       24       66      +42     
Flag Coverage Δ
config-service ?
relay 92.01% <80.00%> (-0.17%) ⬇️
server ?
ws-server ?

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
packages/relay/src/lib/debug.ts 93.34% <100.00%> (-4.45%) ⬇️
packages/relay/src/lib/receiptSerialization.ts 100.00% <100.00%> (ø)
...b/services/ethService/blockService/BlockService.ts 97.65% <100.00%> (-0.66%) ⬇️
.../services/ethService/blockService/IBlockService.ts 100.00% <100.00%> (ø)
...s/relay/src/lib/services/workersService/workers.ts 0.00% <0.00%> (-98.22%) ⬇️
packages/relay/src/lib/types/IReceiptRlpInput.ts 0.00% <0.00%> (ø)
...ib/services/ethService/blockService/blockWorker.ts 92.28% <81.06%> (-6.56%) ⬇️

... and 69 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Contributor

@quiet-node quiet-node left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking better some last items

Comment on lines +428 to +446
const encodedReceiptPromises = contractResults.map(async (contractResult) => {
if (Utils.isRejectedDueToHederaSpecificValidation(contractResult)) {
logger.debug(
`Transaction with hash %s is skipped due to hedera-specific validation failure (%s)`,
contractResult.hash,
contractResult.result,
);
return null;
}

contractResult.logs = logsByHash.get(contractResult.hash) || [];

const receiptRlpInput = createReceiptRlpInput(contractResult.logs, contractResult, blockGasUsedBeforeTransaction);
blockGasUsedBeforeTransaction += contractResult.gas_used;
return encodeReceiptToHex(receiptRlpInput);
});

const resolvedEncodedReceipts = await Promise.all(encodedReceiptPromises);
encodedReceipts.push(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like the encodedReceiptPromises implementation has a misleading pattern. The callbacks in this method are marked as async but contain no await, meaning no actual asynchronous work is being done. Wrapping them in Promise.all provides no concurrency benefit since there is no I/O happening. We should replace this with a regular synchronous loop.

Comment on lines +438 to +441
contractResult.logs = logsByHash.get(contractResult.hash) || [];

const receiptRlpInput = createReceiptRlpInput(contractResult.logs, contractResult, blockGasUsedBeforeTransaction);
blockGasUsedBeforeTransaction += contractResult.gas_used;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code is directly mutating contractResult.logs on a shared input object. Since contractResults is returned by getContractResults and referenced in a subsequently created Set and other local structures, mutating it inside an encoding helper is an unintended side effect and not a good practice.

Instead, we should pass the logs explicitly without patching the object:

Suggested change
contractResult.logs = logsByHash.get(contractResult.hash) || [];
const receiptRlpInput = createReceiptRlpInput(contractResult.logs, contractResult, blockGasUsedBeforeTransaction);
blockGasUsedBeforeTransaction += contractResult.gas_used;
const logs = logsByHash.get(contractResult.hash) || [];
const receiptRlpInput = createReceiptRlpInput(logs, contractResult, blockGasUsedBeforeTransaction);

or

Suggested change
contractResult.logs = logsByHash.get(contractResult.hash) || [];
const receiptRlpInput = createReceiptRlpInput(contractResult.logs, contractResult, blockGasUsedBeforeTransaction);
blockGasUsedBeforeTransaction += contractResult.gas_used;
const receiptRlpInput = createReceiptRlpInput(logsByHash.get(contractResult.hash) || [], contractResult, blockGasUsedBeforeTransaction);

Also worth confirming: contractResult.logs is not actually used inside createReceiptRlpInput(), right?

Comment on lines +511 to +563
/**
* Creates a minimal receipt payload for RLP-encoding of a regular transaction.
*
* Builds an `IReceiptRlpInput` from mirror node contract result data and the
* running cumulative gas used before this transaction. The returned shape
* contains only the fields required for Yellow Paper receipt encoding, including the updated cumulative gas used,
* logs and bloom, root and status, transaction index, and normalized type.
* @param params - Parameters required to build the RLP input, including
* contract result data, associated logs, and the cumulative gas used prior
* to this transaction.
* @returns Minimal receipt data suitable for RLP encoding.
*/
function createReceiptRlpInput(
logs: Log[],
receiptResponse: any,
blockGasUsedBeforeTransaction: number,
): IReceiptRlpInput {
return {
cumulativeGasUsed: numberTo0x(blockGasUsedBeforeTransaction + receiptResponse.gas_used),
logs: logs,
logsBloom: receiptResponse.bloom === constants.EMPTY_HEX ? constants.EMPTY_BLOOM : receiptResponse.bloom,
root: receiptResponse.root || constants.DEFAULT_ROOT_HASH,
status: receiptResponse.status,
transactionIndex: numberTo0x(receiptResponse.transaction_index),
type: nanOrNumberTo0x(receiptResponse.type),
};
}

/**
* Creates a minimal receipt payload for RLP-encoding of a synthetic transaction.
*
* Builds an `IReceiptRlpInput` from synthetic logs only, without resolving any
* addresses or constructing a full `ITransactionReceipt`. The returned shape
* contains the fields required for Yellow Paper receipt encoding, including a zero
* cumulative gas used, zero gas used, a logs bloom computed from the first
* synthetic log, default root and status values, the transaction index from
* the first log, and a fallback type of `0x0`.
*
* @param syntheticLogs - Logs belonging to the synthetic transaction.
* @returns Minimal receipt data suitable for RLP encoding.
*/
function createSyntheticReceiptRlpInput(syntheticLogs: Log[]): IReceiptRlpInput {
return {
cumulativeGasUsed: constants.ZERO_HEX,
logs: syntheticLogs,
logsBloom: LogsBloomUtils.buildLogsBloom(syntheticLogs[0].address, syntheticLogs[0].topics),
root: constants.DEFAULT_ROOT_HASH,
status: constants.ONE_HEX,
transactionIndex: syntheticLogs[0].transactionIndex,
type: constants.ZERO_HEX, // fallback to 0x0 from HAPI transactions
};
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can these two be unified something like

function createReceiptRlpInput(
  logs: Log[],
  receiptResponse?: any,
  blockGasUsedBeforeTransaction?: number,
  isSynthetic: boolean
): IReceiptRlpInput {
  return {
    cumulativeGasUsed: isSynthetic ? ...
    logs,
    logsBloom: isSynthetic ? ...
    root: isSynthetic ? ...
    status: isSynthetic ? ...
    transactionIndex: isSynthetic ? ...
    type: isSynthetic ? ...
  };
}

Or should we keep it separate for readibility?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strange I really thought I left a comment on this file in the previous round of review. Anyhoo should we consider moving this module out of the root of src/lib where the other generic namespace modules live? It feels like the receipt-related logic here would be better encapsulated in a dedicated class rather than sitting alongside the other top-level modules. Since these methods are all doing transaction receipt related work, would it make sense to move them into the TransactionReceiptFactory class? That seems like a natural home for them and would keep things cleaner overall.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add debug_getRawReceipts method

5 participants